﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Objects.DataClasses;
using System.Linq;
using System.Reflection;

/// <summary>
/// A mapper betweeen 2 objects, supports EF objects also.
/// <para>1. Create Map between source and destination object</para>
/// <para>2. Define any custom mapping</para>
/// <example>Eg. SpecifyCustomMap((src, des) => des.Name = src.FirstName + " " + src.LastName)</example>
/// <para>3. Map the objects</para>
/// </summary>
/// <typeparam name="SOURCE">The type of the SOURCE.</typeparam>
/// <typeparam name="DESTINATION">The type of the DESTINATION.</typeparam>
public sealed class Mapper<SOURCE, DESTINATION> : IMapper<SOURCE, DESTINATION>
    where SOURCE : class
    where DESTINATION : class
{

    #region Properties

    private SOURCE src;
    private DESTINATION dest;
    private List<Action<SOURCE, DESTINATION>> CustomMaps = new List<Action<SOURCE, DESTINATION>>();
    private BindingFlags bFlags
    {
        get
        {
            BindingFlags bFlags = BindingFlags.Public | BindingFlags.Instance;
            if (IgnoreCase)
                bFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase;

            return bFlags;
        }
    }

    public bool AutoMapUnspecifiedTargets { get; set; }
    public bool IgnoreCase { get; set; }
    public bool StrictMap { get; set; }
    public List<string> LooseChars { get; set; }
    #endregion

    public Mapper()
    { 
        LooseChars = new List<string>() { "_" };
    }

    public void ValidateMappingConfig()
    {
        if (src == null)
            throw new ArgumentNullException("source");
        if (dest == null)
            throw new ArgumentNullException("destination");
    }

    private bool ValidateEFAttribute(PropertyInfo prop)
    {
        if (prop.PropertyType.BaseType == typeof(EntityReference) || prop.PropertyType.BaseType == typeof(RelatedEnd) ||
            prop.PropertyType == typeof(EntityKey))
        {
            return false;
        }

        return true;
    }

    /// <summary>
    /// Creates the map.
    /// </summary>
    /// <param name="source">The source.</param>
    /// <param name="destination">The destination.</param>
    /// <param name="autoMap">if set to <c>true</c> [auto map].</param>
    /// <param name="ignoreCase">if set to <c>true</c> [ignore case].</param>
    /// <param name="isStrict">if set to <c>true</c> [is strict].</param>
    public void CreateMap(SOURCE source, DESTINATION destination, bool autoMap = false, bool ignoreCase = true, bool isStrict = true)
    {
        src = source;
        dest = destination;

        ValidateMappingConfig();

        AutoMapUnspecifiedTargets = autoMap;
        IgnoreCase = ignoreCase;
        StrictMap = isStrict;
    }

    /// <summary>
    /// Specifies the custom map.
    /// </summary>
    /// <param name="mappingAction">The mapping action.</param>
    public void SpecifyCustomMap(Action<SOURCE, DESTINATION> mappingAction)
    {
        CustomMaps.Add(mappingAction);
    }

    /// <summary>
    /// Maps this instance.
    /// </summary>
    /// <returns></returns>
    public DESTINATION Map()
    {
        ValidateMappingConfig();

        if (AutoMapUnspecifiedTargets)
        {
            IEnumerable<PropertyInfo> destProps = dest.GetType().GetProperties().Where(c => c.CanWrite);
            foreach (PropertyInfo prop in destProps)
            {
                string propName = prop.Name;
                if (!StrictMap)
                    LooseChars.ForEach(c => propName = propName.Replace(c.ToString(), ""));

                if (src.GetType().GetProperty(propName, bFlags) != null &&
                    prop.PropertyType == src.GetType().GetProperty(propName, bFlags).PropertyType)
                {
                    if (src.GetType().BaseType == typeof(EntityObject) && !ValidateEFAttribute(prop))
                        continue;

                    prop.SetValue(dest, src.GetType().GetProperty(propName, bFlags).GetValue(src, null), null);
                }
            }
        }


        CustomMaps.ForEach(c => c.Invoke(src, dest));

        return dest;
    }
}